package com.google.precinct.server;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import com.google.appengine.api.datastore.Blob;
import com.google.appengine.api.datastore.DatastoreService;
import com.google.appengine.api.datastore.DatastoreServiceFactory;
import com.google.appengine.api.datastore.Entity;
import com.google.appengine.api.datastore.Key;
import com.google.appengine.api.memcache.MemcacheService;
import com.google.appengine.api.memcache.MemcacheServiceFactory;

public class PostPrecinctImpl extends HttpServlet {

	private static final long serialVersionUID = 5778222784927847336L;

	// Non-Overwriting (delete first) and Not Compatible with Split Sending
	private static final boolean saveCityCompletion = true;

	// Not Compatible with Split Sending
	private static final boolean savePrecinctInfo = true;

	// COMPATIBLE w/ ALL
	private static final boolean saveAddressInfo = true;

	// Not Compatible with Split Sending
	private static final boolean saveCityInfo = true;

	@SuppressWarnings("unchecked")
	@Override
  protected void doPost(HttpServletRequest req, HttpServletResponse resp)
			throws ServletException, IOException {
		BufferedReader istream = new BufferedReader(new InputStreamReader(req.getInputStream()));
		StringBuilder data = new StringBuilder();
		String dataStr = istream.readLine();
		while (dataStr != null) {
			data.append(dataStr);
			dataStr = istream.readLine();
		}
		try {
			JSONObject dataObj = new JSONObject(data.toString());

			// Read through entire data
			String region = dataObj.getString("region").trim().toUpperCase();
			String province = KeyUtils.cleanProvinceName(dataObj.getString("province")).trim().toUpperCase();
			String city = KeyUtils.cleanCityName(dataObj.getString("city")).trim().toUpperCase();

			Key regionKey = KeyUtils.precinctInfo(region);
			Key cityKey = KeyUtils.precinctInfoCity(
					KeyUtils.precinctInfoProvince(regionKey, province), city);

			List<Entity> entitiesToSave = new ArrayList<Entity>();

			JSONArray barangayRows = dataObj.getJSONArray("data");

			long totalPrecincts = 0;
			long totalClusteredPrecincts = 0;
			long totalCityVoters = 0;
			long clusterId = 1;
			Map<String, Entity> addresses = new HashMap<String, Entity>();

			for (int i = 0; i < barangayRows.length(); i++) {
				JSONObject barangayRow = barangayRows.getJSONObject(i);

				String barangay = barangayRow.getString("name").trim().toUpperCase();
	//				int barangayNum = barangayRow.getInt("num");

				JSONArray precinctCluster = barangayRow.getJSONArray("precinct");
				for (int j = 0; j < precinctCluster.length(); j++) {
					JSONObject precinctClusterRow = precinctCluster.getJSONObject(j);

					totalCityVoters += precinctClusterRow.getLong("clusteredVoterCount");
					totalClusteredPrecincts++;

					if (saveAddressInfo || saveCityInfo || savePrecinctInfo) {
						// Create Cluster information first -- store the final ID in the JSON
						Key clusterKey = KeyUtils.precinctInfoCluster(cityKey, clusterId);clusterId++;
						Entity clusterEnt = new Entity(clusterKey);
						clusterEnt.setProperty("num", precinctClusterRow.getInt("clusterNo"));
						clusterEnt.setProperty("voters", precinctClusterRow.getInt("clusteredVoterCount"));
						clusterEnt.setUnindexedProperty("barangay", barangay);
						clusterEnt.setUnindexedProperty("city", city);
						clusterEnt.setUnindexedProperty("province", province);
						clusterEnt.setUnindexedProperty("region", region);

						String address = precinctClusterRow.getString("address").trim().toUpperCase();
						clusterEnt.setUnindexedProperty("address", address);
						Entity addressEnt = addresses.get(address);
						if (addressEnt == null) {
							addressEnt = new Entity(KeyUtils.addressInfo(cityKey, address));
							addressEnt.setUnindexedProperty("clusterCount", 0);
							addressEnt.setUnindexedProperty("votersCount", 0L);
							addressEnt.setUnindexedProperty("precinctCount", 0);
							addresses.put(address, addressEnt);
						}
						addressEnt.setUnindexedProperty("clusterCount",
								((Integer) addressEnt.getProperty("clusterCount")) + 1);

						int precinctCount = 0;
						StringBuilder precincts = new StringBuilder();
						JSONObject precinctItems = precinctClusterRow.getJSONObject("precinctVoters");
						Iterator precinctItemKeys = precinctItems.keys();
						while (precinctItemKeys.hasNext()) {
							String precinctNo = precinctItemKeys.next().toString().trim().toUpperCase();
							precincts.append(precinctNo + ",");

							// Create the precinct information -- for faster look-up
							Key precinctKey = KeyUtils.precinctInfoPrecinct(cityKey, precinctNo);
							Entity precinctEnt = new Entity(precinctKey);
							precinctEnt.setProperty("voters", precinctItems.getInt(precinctNo));
							precinctEnt.setUnindexedProperty("clusterKey", clusterKey);

							addressEnt.setUnindexedProperty("votersCount",
									((Long) addressEnt.getProperty("votersCount")) + precinctItems.getInt(precinctNo));
							addressEnt.setUnindexedProperty("precinctCount",
									((Integer) addressEnt.getProperty("precinctCount")) + 1);

							precinctCount++;
							if (savePrecinctInfo) {
//								System.out.println("  Precinct : " + precinctNo + ":" + precinctItems.getInt(precinctNo));
								entitiesToSave.add(precinctEnt);
							}
						};
						totalPrecincts += precinctCount;

						clusterEnt.setProperty("precinctCount", precinctCount);
						if (savePrecinctInfo) {
//							System.out.println("  Cluster : " + clusterId);
							entitiesToSave.add(clusterEnt);
						}
						if (saveAddressInfo) {
							for (Entity addressItem : addresses.values()) {
								entitiesToSave.add(addressItem);
							}
						}
					}
				}
			}

			if (saveCityInfo) {
				Entity cityEnt = new Entity(cityKey);
				cityEnt.setProperty("precinctCount", totalPrecincts);
				cityEnt.setProperty("clusteredCount", totalClusteredPrecincts);
				cityEnt.setProperty("voters", totalCityVoters);
				cityEnt.setProperty("addresses", addresses.size());
				entitiesToSave.add(cityEnt);
			}

			// Actually save to datastore
			DatastoreService ds = DatastoreServiceFactory.getDatastoreService();
			int totalItems = entitiesToSave.size();
			while (entitiesToSave.size() > 0) {
				List<Entity> splitSave = new ArrayList<Entity>();
				splitSave = entitiesToSave.subList(0,
						(entitiesToSave.size() > 490 ? 490 : entitiesToSave.size()));
				entitiesToSave = entitiesToSave.subList(
						(entitiesToSave.size() > 490 ? 490 : entitiesToSave.size()),
						entitiesToSave.size());
				ds.put(splitSave);
				totalItems -= splitSave.size();
			}
			if (totalItems != 0) {
				System.out.println("WARNING!!!!  Not all entries are saved.");
			}

			// Get Keys for the Autocomplete
			if (saveCityCompletion) {
				populateCityCompletion(ds, region, province, city, totalCityVoters);
			}

			resp.setContentType("text/plain");
			resp.setStatus(200);
		} catch (JSONException e) {
			sendError(resp, "Invalid Data Format");
		}
	}











	private void populateCityCompletion(DatastoreService ds, String region, String province,
			String city, long totalCityVoters) {
		Key[] cityCmplteKeys = new Key[] {
				KeyUtils.cityCompletion(city, 0),
				KeyUtils.cityCompletion(city, 1),
				KeyUtils.cityCompletion(city, 2),
			};
		ArrayList<Entity> entitiesToSave = new ArrayList<Entity>();
		Map<Key, Entity> cityCmplteData = ds.get( Arrays.asList(cityCmplteKeys) );
		Entity L1 = initializedCityCmplte(cityCmplteData, cityCmplteKeys[0]);
		boolean updateMemcache = false;
		if (injectSuggestion(L1, region, province, city, totalCityVoters, 20)) {
			entitiesToSave.add(L1);
			updateMemcache = true;
		}
		Entity L2 = initializedCityCmplte(cityCmplteData, cityCmplteKeys[1]);
		if (injectSuggestion(L2, region, province, city, totalCityVoters, 20)) {
			entitiesToSave.add(L2);
		}
		Entity L3 = initializedCityCmplte(cityCmplteData, cityCmplteKeys[2]);
		if (injectSuggestion(L3, region, province, city, totalCityVoters, 500)) {
			entitiesToSave.add(L3);
		}
		if (entitiesToSave.size() > 0) {
			ds.put(entitiesToSave);

			// Update Memcache Too
			if (updateMemcache) {
				MemcacheService mem = MemcacheServiceFactory.getMemcacheService();
				mem.put(KeyUtils.memcache(L1.getKey()), L1.getProperty("completion"));
			}
		}

	}

	private boolean injectSuggestion(Entity entity, String region, String province,
			String city, long totalCityVoters, int maxLength) {
		try {
			// Parse the JSON Object and find if its injectable
			JSONObject data = new JSONObject(new String(((Blob) entity.getProperty("completion")).getBytes()));
			JSONArray sortedList = data.getJSONArray("list");

			if (maxLength > sortedList.length() ||
					(Long) entity.getProperty("min") < totalCityVoters) {

				while (sortedList.length() > maxLength) {
					sortedList.remove(sortedList.length() - 1);
				}
				JSONObject newEntry = new JSONObject();
				newEntry.put("R", region);
				newEntry.put("P", province);
				newEntry.put("C", city);
				newEntry.put("voters", totalCityVoters);

				int i = sortedList.length();
				long newMin = (i <= 0 ? totalCityVoters : sortedList.getJSONObject(i - 1).getLong("voters"));
				while (i > 0 && sortedList.getJSONObject(i - 1).getLong("voters") < totalCityVoters) {
					sortedList.put(i, sortedList.getJSONObject(i - 1));
					i--;
				}
//				System.out.println("  Insert (" + i + ")  [" + suggestText + "]   " + totalCityVoters);
				sortedList.put(i, newEntry);
				entity.setUnindexedProperty("completion", new Blob(data.toString().getBytes()));
				entity.setUnindexedProperty("min", newMin);
				return true;
			}
		} catch (JSONException e) {
			System.out.println("JSON Exception! " + e.getMessage());
			throw new RuntimeException(e);
		}
		return false;
	}

	private Entity initializedCityCmplte(Map<Key, Entity> cityCmplteData, Key key) {
		if (!cityCmplteData.containsKey(key)) {
			Entity cmplte = new Entity(key);
			JSONObject completion = new JSONObject();
			try {
				completion.put("list", new JSONArray());
			} catch (JSONException e) {
					throw new RuntimeException(e);
			}
			cmplte.setUnindexedProperty("completion",
					new Blob(completion.toString().getBytes()));
			cmplte.setUnindexedProperty("min", 0l);
			cityCmplteData.put(key, cmplte);
		}
		return cityCmplteData.get(key);
	}

	private void sendError(HttpServletResponse resp, String message) {
		try {
			resp.sendError(500, message);
		} catch (IOException e) {
		}
	}

}
